package com.hunttown.common.dao.datasource.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.hunttown.common.dao.datasource.MasterSlaveDataSource;
import com.hunttown.common.dao.transaction.TransactionManagers;

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;

public class MasterSlaveDataSourceBeanDefinitionParser implements BeanDefinitionParser {
    private static final Logger logger = LoggerFactory.getLogger(MasterSlaveDataSourceBeanDefinitionParser.class);
    private Class DRUID_DATASOURCE_CLASS;
    private final Class READ_WRITE_DATASOURCE_CLASS = MasterSlaveDataSource.class;
    private final Class TRANSACTION_MANAGER_CLASS = DataSourceTransactionManager.class;
    private final Class TRANSACTION_MANAGERS_CLASS = TransactionManagers.class;
    private final int DATASOURCE_INIT_SIZE = 5;
    private final int DATASOURCE_MAX_SIZE = 100;

    public MasterSlaveDataSourceBeanDefinitionParser() {
        try {
            this.DRUID_DATASOURCE_CLASS = DruidDataSource.class;
        } catch (Throwable e) {
        }
    }

    public BeanDefinition parse(Element element, ParserContext parserContext) {
        logger.debug("datasource-config : ==========start all datasource config==========");
        if (this.DRUID_DATASOURCE_CLASS == null) {
            throw new IllegalStateException("please add druid datasource dependency");
        }
        List<Element> datasourceElements = DomUtils.getChildElementsByTagName(element, "datasource");
        parseDataSources(datasourceElements, parserContext);

        String defaultDataSourceName = getDefaultDataSourceBeanName(datasourceElements);
        registerDefaultDataSourceToTransactionManagers(defaultDataSourceName, parserContext);

        logger.debug("datasource-config : ==========end all datasource config==========");
        return null;
    }

    private void registerDefaultDataSourceToTransactionManagers(String defaultDataSourceName, ParserContext parserContext) {
        BeanDefinition transactionManagerDefinition = null;
        if (parserContext.getRegistry().containsBeanDefinition("transactionManagers")) {
            transactionManagerDefinition = parserContext.getRegistry().getBeanDefinition("transactionManagers");
        } else {
            transactionManagerDefinition = new RootBeanDefinition(this.TRANSACTION_MANAGERS_CLASS);
            parserContext.getRegistry().registerBeanDefinition("transactionManagers", transactionManagerDefinition);
        }
        transactionManagerDefinition.getPropertyValues().add("defaultDataSource", new RuntimeBeanReference(defaultDataSourceName));
    }

    private void registerMappingToTransactionManagers(String dataSourceName, String transactionManagerNamne, ParserContext parserContext) {
        BeanDefinition transactionManagerDefinition = null;
        if (parserContext.getRegistry().containsBeanDefinition("transactionManagers")) {
            transactionManagerDefinition = parserContext.getRegistry().getBeanDefinition("transactionManagers");
        } else {
            transactionManagerDefinition = new RootBeanDefinition(this.TRANSACTION_MANAGERS_CLASS);
            parserContext.getRegistry().registerBeanDefinition("transactionManagers", transactionManagerDefinition);
        }
        PropertyValues propertyValues = transactionManagerDefinition.getPropertyValues();

        ManagedMap<RuntimeBeanReference, RuntimeBeanReference> transactionManagerMap = null;
        ManagedMap<RuntimeBeanReference, String> transactionManagerBeanNameMap = null;
        ManagedMap<RuntimeBeanReference, String> dataSourceBeanNameMap = null;
        PropertyValue transactionManagerMapValue = propertyValues.getPropertyValue("transactionManagerMap");

        PropertyValue transactionManagerBeanNameMapValue = propertyValues.getPropertyValue("transactionManagerBeanNameMap");

        PropertyValue dataSourceBeanNameMapValue = propertyValues.getPropertyValue("dataSourceBeanNameMap");
        if (transactionManagerMapValue != null) {
            transactionManagerMap = (ManagedMap) transactionManagerMapValue.getValue();
            transactionManagerBeanNameMap = (ManagedMap) transactionManagerBeanNameMapValue.getValue();
            dataSourceBeanNameMap = (ManagedMap) dataSourceBeanNameMapValue.getValue();
        }
        if (transactionManagerMap == null) {
            transactionManagerMap = new ManagedMap();
            transactionManagerBeanNameMap = new ManagedMap();
            dataSourceBeanNameMap = new ManagedMap();
            transactionManagerDefinition.getPropertyValues().add("transactionManagerMap", transactionManagerMap);
            transactionManagerDefinition.getPropertyValues().add("transactionManagerBeanNameMap", transactionManagerBeanNameMap);
            transactionManagerDefinition.getPropertyValues().add("dataSourceBeanNameMap", dataSourceBeanNameMap);
        }
        transactionManagerMap.put(new RuntimeBeanReference(dataSourceName), new RuntimeBeanReference(transactionManagerNamne));

        transactionManagerBeanNameMap.put(new RuntimeBeanReference(transactionManagerNamne), transactionManagerNamne);

        dataSourceBeanNameMap.put(new RuntimeBeanReference(dataSourceName), dataSourceName);
    }

    private void parseDataSources(List<Element> datasourceElements, ParserContext parserContext) {
        for (Element datasourceElement : datasourceElements) {
            logger.debug("master/slave datasource config : ==========start config==========");
            parseDataSource(null, datasourceElement, parserContext);
            logger.debug("master/slave datasource config : ==========end config==========");
        }
    }

    private void parseShardingDataSource(List<Element> shardingDatasourceElements, ParserContext parserContext) {
        logger.debug("sharding datasource config : start config");
        for (Element shardingDatasourceElement : shardingDatasourceElements) {
            List<Element> datasourceElements = DomUtils.getChildElementsByTagName(shardingDatasourceElement, "datasource");
            parseDataSources(datasourceElements, parserContext);
        }
        logger.debug("master/slave datasource config : end config");
    }

    private String parseDataSource(String beanName, Element datasourceElement, ParserContext parserContext) {
        if (StringUtils.isBlank(beanName)) {
            beanName = datasourceElement.getAttribute("id");
        }
        String url = datasourceElement.getAttribute("url");
        String refBeanName = datasourceElement.getAttribute("ref");
        if ((StringUtils.isNotBlank(url)) && (StringUtils.isNotBlank(refBeanName))) {
            throw new IllegalArgumentException("datasource-config : datasource tag attribute [url/username/password/maxPoolSize] and [ref] can set one, cannot both set!");
        }
        List<Element> slaveDataSourceElements = DomUtils.getChildElementsByTagName(datasourceElement, "slave");
        if (slaveDataSourceElements.isEmpty()) {
            if (StringUtils.isNotBlank(refBeanName)) {
                boolean isNotSlave = !"slave".equals(datasourceElement.getLocalName());
                if ((isNotSlave) && (refBeanName.equals(beanName))) {
                    throw new IllegalArgumentException("master/slave datasource config : happen circle reference, datasource bean name : " + beanName);
                }
                logger.debug("master/slave datasource config : ref datasource bean : {}", refBeanName);
                registerTransactionManagerDefinition(refBeanName, datasourceElement, parserContext);
                return refBeanName;
            }
            registerDataSourceDefinition(beanName, datasourceElement, parserContext);
            return beanName;
        }
        RootBeanDefinition masterSlaveDatasourceDefinition = new RootBeanDefinition(this.READ_WRITE_DATASOURCE_CLASS);
        String masterBeanName = null;
        if (StringUtils.isNotBlank(refBeanName)) {
            if (refBeanName.equals(beanName)) {
                throw new IllegalArgumentException("master/slave datasource config : happen circle reference, datasource bean name : " + beanName);
            }
            masterBeanName = refBeanName;
            logger.debug("master/slave datasource config : ref datasource bean : " + masterBeanName);
            registerTransactionManagerDefinition(masterBeanName, datasourceElement, parserContext);
        } else {
            masterBeanName = generateMasterDataSourceBeanName(beanName);
            registerDataSourceDefinition(masterBeanName, datasourceElement, parserContext);
        }
        logger.debug("master/slave datasource config : master datasource bean : {}", masterBeanName);

        ManagedList<RuntimeBeanReference> slaveDataSourceList = new ManagedList();
        ManagedList<String> slaveDataSourceBeanNameList = new ManagedList();
        int i = 0;
        for (Element slaveDataSourceElement : slaveDataSourceElements) {
            String slaveBeanName = generateSlaveDataSourceBeanName(beanName, i++);
            slaveBeanName = parseDataSource(slaveBeanName, slaveDataSourceElement, parserContext);
            logger.debug("master/slave datasource config : slave datasource bean : {}", slaveBeanName);
            slaveDataSourceList.add(new RuntimeBeanReference(slaveBeanName));
            slaveDataSourceBeanNameList.add(slaveBeanName);
        }
        masterSlaveDatasourceDefinition.getPropertyValues().add("masterDataSource", new RuntimeBeanReference(masterBeanName));
        masterSlaveDatasourceDefinition.getPropertyValues().add("masterDataSourceBeanName", masterBeanName);
        masterSlaveDatasourceDefinition.getPropertyValues().add("slaveDataSources", slaveDataSourceList);
        masterSlaveDatasourceDefinition.getPropertyValues().add("slaveDataSourceBeanNames", slaveDataSourceBeanNameList);

        parserContext.getRegistry().registerBeanDefinition(beanName, masterSlaveDatasourceDefinition);

        return beanName;
    }

    private void registerDataSourceDefinition(String beanName, Element datasourceElement, ParserContext parserContext) {
        String url = datasourceElement.getAttribute("url");
        String username = datasourceElement.getAttribute("username");
        String password = datasourceElement.getAttribute("password");
        String maxPoolSize = datasourceElement.getAttribute("max-pool-size");

        RootBeanDefinition dataSourceDefinition = new RootBeanDefinition(this.DRUID_DATASOURCE_CLASS);
        MutablePropertyValues propertyValues = dataSourceDefinition.getPropertyValues();
        propertyValues.add("url", url);
        propertyValues.add("username", username);
        propertyValues.add("password", password);
        propertyValues.add("initialSize", Integer.valueOf(5));
        propertyValues.add("maxActive", StringUtils.isNotBlank(maxPoolSize) ? maxPoolSize : Integer.valueOf(100));
        if (StringUtils.isBlank(beanName)) {
            beanName = BeanDefinitionReaderUtils.generateBeanName(dataSourceDefinition, parserContext.getRegistry());
        }
        parserContext.getRegistry().registerBeanDefinition(beanName, dataSourceDefinition);

        registerTransactionManagerDefinition(beanName, datasourceElement, parserContext);

        logger.debug("create datasource bean : {}", beanName);
    }

    private String registerTransactionManagerDefinition(String dataSourceBeanName, Element dataSourceElement, ParserContext parserContext) {
        String transactionManagerBeanName = dataSourceElement.getAttribute("transaction-manager");
        if (StringUtils.isBlank(transactionManagerBeanName)) {
            RootBeanDefinition transactionManagerDefinition = new RootBeanDefinition(this.TRANSACTION_MANAGER_CLASS);
            transactionManagerDefinition.getPropertyValues().add("dataSource", new RuntimeBeanReference(dataSourceBeanName));
            transactionManagerBeanName = generateTransactionManagerBeanName(dataSourceBeanName);
            parserContext.getRegistry().registerBeanDefinition(transactionManagerBeanName, transactionManagerDefinition);
            logger.debug("create datasource transactionManager bean : {}", transactionManagerBeanName);
        } else {
            if (!parserContext.getRegistry().containsBeanDefinition(transactionManagerBeanName)) {
                throw new IllegalArgumentException("transaction-manager bean [" + transactionManagerBeanName + "] dont`s exists");
            }
            logger.debug("ref datasource transactionManager bean : {}", transactionManagerBeanName);
        }
        registerMappingToTransactionManagers(dataSourceBeanName, transactionManagerBeanName, parserContext);
        return transactionManagerBeanName;
    }

    public String getDefaultDataSourceBeanName(List<Element> datasourceElements) {
        List<Element> defaultElements = new ArrayList();
        for (Element datasourceElement : datasourceElements) {
            if ("true".equalsIgnoreCase(datasourceElement.getAttribute("default"))) {
                defaultElements.add(datasourceElement);
            }
        }
        if (defaultElements.size() != 1) {
            throw new IllegalArgumentException("datasource-config : must have 1 default='true' datasource definition, but find [" + defaultElements.size() + "]");
        }
        String beanName = ((Element) defaultElements.get(0)).getAttribute("id");
        logger.debug("datasource-config : default datasource bean : {}", beanName);
        return beanName;
    }

    private String generateTransactionManagerBeanName(String dataSourceBeanName) {
        return dataSourceBeanName + "_TransactionManager";
    }

    private String generateMasterDataSourceBeanName(String dataSourceBeanName) {
        return dataSourceBeanName + "_master";
    }

    private String generateSlaveDataSourceBeanName(String dataSourceBeanName, int index) {
        return dataSourceBeanName + "_slave" + index;
    }
}
